<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>自定义双向绑定</title>
  </head>
  <body>
    <div id="app">
      <input type="text" v-model="text" />
      {{text}}
    </div>
  </body>
  <script>
    // 观察者，进行观察数据变化
    function observe(obj) {
      for (const key in obj) {
        defaultReactive(obj, key, obj[key]);
      }
    }
    function defaultReactive(obj, key, val) {
      // 每个属性都有一个dep
      let dep = new Dep();
      Object.defineProperty(obj, key, {
        get: function () {
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return val;
        },
        set: function (newVal) {
          if (newVal == val) {
            return;
          }
          val = newVal;
          // 数据一旦变化，就要通知改变试图，及要告诉订阅者
          dep.notify();
        },
      });
    }
    // 存放多个订阅者
    function Dep() {
      this.subs = [];
    }
    Dep.prototype = {
      // 存放订阅者
      addSub(sub) {
        this.subs.push(sub);
      },
      // 触发订阅者
      notify() {
        this.subs.forEach((item) => {
          item.update();
        });
      },
    };
    // 订阅者
    function Watcher(vm, node, name) {
      Dep.target = this;
      this.vm = vm;
      this.node = node;
      this.name = name;
      this.update();
      Dep.target = null;
    }
    Watcher.prototype = {
      update() {
        this.node.nodeValue = this.vm.data[this.name];
      },
    };
    // 文档碎片，一次插入大量节点
    function nodeToFragment(node, vm) {
      let fragment = document.createDocumentFragment();
      while (node.firstChild) {
        compile(node.firstChild, vm);
        fragment.appendChild(node.firstChild);
      }
      return fragment;
    }

    // 编译内容
    function compile(node, vm) {
      switch (node.nodeType) {
        // 元素节点
        case 1:
          let atrr = node.attributes;
          for (let i = 0; i < atrr.length; i++) {
            if (atrr[i].nodeName === "v-model") {
              let name = atrr[i].nodeValue;
              // 监听键盘
              node.addEventListener("input", function (e) {
                vm.data[name] = e.target.value;
              });
              node.value = vm.data[name];
            }
          }
          break;
        // 文本节点
        case 3:
          // 正则匹配{{}}
          let reg = /\{\{(.*)\}\}/;
          if (reg.test(node.nodeValue)) {
            let name = RegExp.$1;
            // node.nodeValue = vm.data[name];
            new Watcher(vm, node, name);
          }
          break;
        default:
          break;
      }
    }
    function Vue(options) {
      this.data = options.data;
      this.el = options.el;
      observe(this.data);
      let dom = nodeToFragment(document.getElementById(this.el), this);
      document.getElementById(this.el).appendChild(dom);
    }
    new Vue({
      data: {
        text: "hello world",
      },
      el: "app",
    });
  </script>
</html>
